package cz.data.domain.market.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cz.data.commo.office.word.WordUtil;
import cz.data.common.base.BaseResponse;
import cz.data.common.base.SimpleDataResponse;
import cz.data.common.mybatis.BaseServiceImpl;
import cz.data.common.core.DataConstant;
import cz.data.common.exception.DataException;
import cz.data.common.rabbitmq.config.RabbitConstant;
import cz.data.common.utils.MD5Util;
import cz.data.common.utils.SecurityUtil;
import cz.data.common.utils.ThrowableUtil;
import com.aspose.words.Document;
import com.aspose.words.MailMerge;
import com.aspose.words.net.System.Data.DataRow;
import com.aspose.words.net.System.Data.DataTable;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import cz.data.domain.market.dao.DataApiDao;
import cz.data.domain.market.model.dto.ApiChangeMessage;
import cz.data.domain.market.model.entity.DataApiEntity;
import cz.data.domain.market.mapstruct.DataApiMapper;
import cz.data.domain.market.model.dto.DataApiDto;
import cz.data.domain.market.model.dto.ReqParam;
import cz.data.domain.market.model.dto.ResParam;
import cz.data.domain.market.model.dto.SqlParseDto;
import cz.data.domain.market.model.enums.ConfigType;
import cz.data.domain.market.model.vo.ApiHeader;
import cz.data.domain.market.model.vo.SqlParseVo;
import cz.data.domain.market.service.DataApiManager;
import cz.data.domain.market.service.DataApiService;
import cz.data.domain.market.utils.MultipleStatementVisitorAdapter;
import cz.data.domain.market.utils.SingleStatementVisitorAdapter;
import cz.data.domain.market.utils.SqlBuilderUtil;
import cz.data.domain.metadata.model.entity.MetadataAuthorizeEntity;
import cz.data.domain.metadata.model.entity.MetadataColumnEntity;
import cz.data.domain.metadata.model.entity.MetadataSourceEntity;
import cz.data.domain.metadata.model.entity.MetadataTableEntity;
import cz.data.domain.metadata.model.enums.DataLevel;
import cz.data.domain.metadata.rpc.MetadataFeign;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.JdbcNamedParameter;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.SelectUtils;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class DataApiServiceImpl extends BaseServiceImpl<DataApiDao, DataApiEntity> implements DataApiService {

    @Autowired
    DataApiDao dataApiDao;
    @Autowired
    DataApiMapper dataApiMapper;
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Autowired
    ObjectMapper objectMapper;
    @Autowired
    MetadataFeign metadataFeign;
    @Lazy
    @Resource
    DataApiManager dataApiManager;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveDataApi(DataApiDto dataApiDto) {
        DataApiEntity dataApi = shareCode(dataApiDto);
        dataApiDao.insert(dataApi);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateDataApi(DataApiDto dataApiDto) {
        DataApiEntity dataApi = shareCode(dataApiDto);
        dataApiDao.updateById(dataApi);
    }

    private DataApiEntity shareCode(DataApiDto dataApiDto) {
        DataApiEntity dataApi = dataApiMapper.toEntity(dataApiDto);
        boolean existsApi = dataApiManager.existsApi(dataApi);
        if (existsApi) {
            throw new DataException("API路径和请求方式已经存在");
        }
        String configType = dataApi.getExecuteConfig().getConfigType();
        if (ConfigType.FORM.getKey().equals(configType)) {
            try {
                dataApi.getExecuteConfig().setSqlText(sqlJdbcNamedParameterBuild(dataApi));
            } catch (JSQLParserException e) {
                log.error("全局异常信息ex={}, StackTrace={}", e.getMessage(), ThrowableUtil.getStackTrace(e));
                throw new DataException("SQL语法有问题，解析出错");
            }
        } else if (ConfigType.SCRIPT.getKey().equals(configType)) {
        }
        return dataApi;
    }

    @Override
    public DataApiEntity getDataApiById(String id) {
        return super.getById(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteDataApiById(String id) {
        int rows = dataApiDao.deleteById(id);
        if (rows > 1) {
            JSONObject payload = new JSONObject();
            payload.put("id", id);
            payload.put("metadata", new JSONObject().fluentPut("_remove", "1"));
            rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE,
                    RabbitConstant.TOPIC_GATEWAY_ADMIN_ROUTE, payload.toJSONString());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteDataApiBatch(List<String> ids) {
        dataApiDao.deleteBatchIds(ids);
        for (String id : ids) {
            JSONObject payload = new JSONObject();
            payload.put("id", id);
            payload.put("metadata", new JSONObject().fluentPut("_remove", "1"));
            rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE,
                    RabbitConstant.TOPIC_GATEWAY_ADMIN_ROUTE, payload.toJSONString());
        }
    }

    @Override
    public DataApiEntity getDataApiByMethodAndApiUrl(String method, String apiVersion, String apiUrl) {
        QueryWrapper<DataApiEntity> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(DataApiEntity::getReqMethod, method)
                .eq(DataApiEntity::getApiVersion, apiVersion)
                .eq(DataApiEntity::getApiUrl, apiUrl);
        return getOne(wrapper);
    }

    @Override
    public SqlParseVo sqlParse(SqlParseDto sqlParseDto) {
        String sourceId = sqlParseDto.getSourceId();
        String sql = sqlParseDto.getSqlText();
        sql = sql.replace(SqlBuilderUtil.getInstance().MARK_KEY_START, "");
        sql = sql.replace(SqlBuilderUtil.getInstance().MARK_KEY_END, "");
        Statement stmt;
        try {
            stmt = CCJSqlParserUtil.parse(sql);
        } catch (JSQLParserException e) {
//            log.error("全局异常信息ex={}, StackTrace={}", e.getMessage(), ThrowableUtil.getStackTrace(e));
            throw new DataException("SQL语法有问题，解析出错", e);
        }
        // 维护元数据缓存数据
        TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
        List<String> tables = tablesNamesFinder.getTableList(stmt);
        // 查询字段
        final List<Map<String, String>> cols = new ArrayList<>();
        // 查询参数
        final List<String> vars = new ArrayList<>();
        if (tables.size() == 1) {
            // 单表解析
            singleSqlParse(stmt, cols, vars, tables.get(0));
        } else if (tables.size() > 1) {
            // 多表解析
            multipleSqlParse(stmt, cols, vars);
        }
        SqlParseVo sqlParseVo = new SqlParseVo();
        List<ReqParam> reqParams = vars.stream().map(s -> {
            ReqParam reqParam = new ReqParam();
            reqParam.setParamName(s);
            reqParam.setNullable(DataConstant.TrueOrFalse.FALSE.getKey());
            return reqParam;
        }).collect(Collectors.toList());
        sqlParseVo.setReqParams(reqParams);
        List<ResParam> resParams = new ArrayList<>();
        MetadataSourceEntity sourceEntity = metadataFeign.getMetadataSourceById(sourceId);
        boolean admin = SecurityUtil.isAdmin();
        if (sourceEntity != null) {
            List<MetadataTableEntity> tableEntityList = metadataFeign.getMetadataTableBySourceId(sourceEntity.getId());
            Map<String, List<Map<String, String>>> map = cols.stream().collect(Collectors.groupingBy(e -> e.get("tableName")));
            for (Map.Entry<String, List<Map<String, String>>> entry : map.entrySet()) {
                String entryKey = entry.getKey().toLowerCase();
                List<Map<String, String>> entryValue = entry.getValue();
                MetadataTableEntity tableEntity = tableEntityList.stream().filter(t -> entryKey.equals(t.getTableName().toLowerCase())).findFirst().orElse(null);
                if (tableEntity != null) {
                    List<MetadataColumnEntity> columnEntityList = metadataFeign.getMetadataColumnByTableId(tableEntity.getId());
                    for (Map<String, String> m : entryValue) {
                        String columnName = m.get("columnName").toLowerCase();
                        String columnAliasName = m.get("columnAliasName");
                        Stream<MetadataColumnEntity> stream = columnEntityList.stream();
                        if (!"*".equals(columnName)) {
                            stream = stream.filter(c -> columnName.equals(c.getColumnName().toLowerCase()));
                        } else {
                            columnAliasName = null;
                        }
                        if (!admin) {
                            Set<String> set = new HashSet<>();
                            List<String> roleIds = SecurityUtil.getUserRoleIds();
                            roleIds.forEach(role -> {
                                List<MetadataAuthorizeEntity> list = metadataFeign.getMetadataAuthorizeByRoleId(role);
                                set.addAll(Optional.ofNullable(list)
                                        .orElseGet(Collections::emptyList)
                                        .stream()
                                        .filter(s -> Objects.equals(DataLevel.COLUMN.getKey(), s.getObjectType()))
                                        .map(MetadataAuthorizeEntity::getObjectId)
                                        .collect(Collectors.toSet()));
                            });
                            stream = stream.filter(s -> set.contains(s.getId()));
                        }
                        String finalColumnAliasName = columnAliasName;
                        stream.map(c -> MetadataColumnToResParam(c, finalColumnAliasName)).forEach(resParams::add);
                    }
                }
            }
        }
        if (stmt instanceof Insert ||
                stmt instanceof Update ||
                stmt instanceof Delete) {
            ResParam resParam = new ResParam();
            resParam.setFieldName("rows");
            resParam.setDataType("int");
            resParam.setFieldComment("影响行数");
            resParam.setExampleValue("0");
            resParams.add(resParam);
        }
        sqlParseVo.setResParams(resParams);
        return sqlParseVo;
    }

    private ResParam MetadataColumnToResParam(MetadataColumnEntity columnEntity, String columnAliasName) {
        ResParam resParam = new ResParam();
        resParam.setFieldName(columnEntity.getColumnName());
        resParam.setFieldComment(StringUtils.isNotBlank(columnEntity.getColumnComment()) ? columnEntity.getColumnComment() : "");
        resParam.setDataType(StringUtils.isNotBlank(columnEntity.getDataType()) ? columnEntity.getDataType() : "");
        resParam.setFieldAliasName(StringUtils.isNotBlank(columnAliasName) ? columnAliasName : "");
        return resParam;
    }

    private void singleSqlParse(Statement stmt, List<Map<String, String>> cols, List<String> vars, String tableName) {
        stmt.accept(new SingleStatementVisitorAdapter(cols, vars, tableName));
    }

    private void multipleSqlParse(Statement stmt, List<Map<String, String>> cols, List<String> vars) {
        stmt.accept(new MultipleStatementVisitorAdapter(cols, vars));
    }

    private String sqlJdbcNamedParameterBuild(DataApiEntity dataApi) throws JSQLParserException {
        Table table = new Table(dataApi.getExecuteConfig().getTableName());
        String[] resParams = dataApi.getResParams().stream().map(ResParam::getFieldName).toArray(String[]::new);
        Select select = SelectUtils.buildSelectFromTableAndExpressions(table, resParams);
        return SqlBuilderUtil.getInstance().buildHql(select.toString(), dataApi.getReqParams());
    }

    private String sqlJdbcNamedParameterParse(String sqlText) throws JSQLParserException {
        final StringBuilder buffer = new StringBuilder();
        ExpressionDeParser expressionDeParser = new ExpressionDeParser() {
            @Override
            public void visit(JdbcNamedParameter jdbcNamedParameter) {
                this.getBuffer().append("?");
            }
        };
        SelectDeParser deparser = new SelectDeParser(expressionDeParser, buffer);
        expressionDeParser.setSelectVisitor(deparser);
        expressionDeParser.setBuffer(buffer);
        Statement stmt = CCJSqlParserUtil.parse(sqlText);
        stmt.accept(new StatementVisitorAdapter() {
            @Override
            public void visit(Select select) {
                select.getSelectBody().accept(deparser);
            }
        });
        return buffer.toString();
    }

    @Override
    public void copyDataApi(String id) {
        DataApiEntity dataApiEntity = Optional.ofNullable(super.getById(id)).orElseThrow(() -> new DataException("获取失败"));
        DataApiEntity copy = new DataApiEntity();
        String dateFormat = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        copy.setApiName(dataApiEntity.getApiName() + "_副本" + dateFormat);
        copy.setApiVersion(dataApiEntity.getApiVersion());
        copy.setApiUrl(dataApiEntity.getApiUrl() + "/copy" + dateFormat);
        copy.setReqMethod(dataApiEntity.getReqMethod());
        copy.setResType(dataApiEntity.getResType());
        copy.setDeny(dataApiEntity.getDeny());
        copy.setRateLimit(dataApiEntity.getRateLimit());
        copy.setExecuteConfig(dataApiEntity.getExecuteConfig());
        copy.setReqParams(dataApiEntity.getReqParams());
        copy.setResParams(dataApiEntity.getResParams());
        copy.setStatus(DataConstant.ApiState.WAIT.getKey());
        dataApiDao.insert(copy);
    }

    @Override
    public void releaseDataApi(String id) {
        LambdaUpdateWrapper<DataApiEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(DataApiEntity::getStatus, DataConstant.ApiState.RELEASE.getKey());
        updateWrapper.eq(DataApiEntity::getId, id);
        dataApiDao.update(null, updateWrapper);
        rabbitTemplate.convertAndSend(RabbitConstant.API_FANOUT_EXCHANGE, "",
                ApiChangeMessage.fromDataApi(id, ApiChangeMessage.RELEASE));
    }

    @Override
    public void cancelDataApi(String id) {
        LambdaUpdateWrapper<DataApiEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(DataApiEntity::getStatus, DataConstant.ApiState.CANCEL.getKey());
        updateWrapper.eq(DataApiEntity::getId, id);
        dataApiDao.update(null, updateWrapper);
        rabbitTemplate.convertAndSend(RabbitConstant.API_FANOUT_EXCHANGE, "",
                ApiChangeMessage.fromDataApi(id, ApiChangeMessage.CANCEL));
    }

    @Override
    public Document wordDataApi(String id) throws Exception {
        DataApiEntity dataApiEntity = super.getById(id);
        // 合并模版
        ClassPathResource classPathResource = new ClassPathResource("templates/api_1.0.0.docx");
        InputStream inputStream = classPathResource.getInputStream();
        Document doc = WordUtil.getInstance().getDocument(inputStream);
        // 提供字段
        MD5Util mt = MD5Util.getInstance();
        String apiKey = mt.encode(id);
        String secretkey = mt.encode(SecurityUtil.getUserId());
        String[] fieldNames = new String[]{"apiName", "apiVersion", "reqMethod", "resType", "apiUrl", "remark", "apiKey", "secretkey"};
        Object[] fieldValues = new Object[]{dataApiEntity.getApiName(), dataApiEntity.getApiVersion(), dataApiEntity.getReqMethod(), dataApiEntity.getResType(), "/services/" + dataApiEntity.getApiVersion() + dataApiEntity.getApiUrl(), dataApiEntity.getRemark(), apiKey, secretkey};
        MailMerge mailMerge = doc.getMailMerge();
        mailMerge.execute(fieldNames, fieldValues);
        // 请求参数 TableStart:ReqParamList TableEnd:ReqParamList
        DataTable reqParamTable = new DataTable("ReqParamList");
        reqParamTable.getColumns().add("paramName");
        reqParamTable.getColumns().add("paramComment");
        reqParamTable.getColumns().add("paramType");
        reqParamTable.getColumns().add("nullable");
        reqParamTable.getColumns().add("exampleValue");
        List<ReqParam> reqParamList = objectMapper.convertValue(dataApiEntity.getReqParams(), new TypeReference<List<ReqParam>>() {
        });
        for (ReqParam reqParam : reqParamList) {
            DataRow row = reqParamTable.newRow();
            row.set(0, reqParam.getParamName());
            row.set(1, reqParam.getParamComment());
            row.set(2, reqParam.getParamType());
            row.set(3, "1".equals(reqParam.getNullable()) ? "Y" : "N");
            row.set(4, reqParam.getExampleValue());
            reqParamTable.getRows().add(row);
        }
        mailMerge.executeWithRegions(reqParamTable);
        // 返回字段 TableStart:ResParamList TableEnd:ResParamList
        DataTable resParamTable = new DataTable("ResParamList");
        resParamTable.getColumns().add("fieldName");
        resParamTable.getColumns().add("dataType");
        resParamTable.getColumns().add("fieldComment");
        resParamTable.getColumns().add("exampleValue");
        List<ResParam> resParamList = objectMapper.convertValue(dataApiEntity.getResParams(), new TypeReference<List<ResParam>>() {
        });
        for (ResParam resParam : resParamList) {
            DataRow row = resParamTable.newRow();
            row.set(0, resParam.getFieldName());
            row.set(1, resParam.getDataType());
            row.set(2, resParam.getFieldComment());
            row.set(3, resParam.getExampleValue());
            resParamTable.getRows().add(row);
        }
        mailMerge.executeWithRegions(resParamTable);
        // 返回示例 reqExample resExample
        ObjectNode objectNode = objectMapper.createObjectNode();
        for (ResParam param : resParamList) {
            objectNode.put(param.getFieldName(), param.getExampleValue());
        }
        mailMerge.execute(new String[]{"reqExample", "resExample"},
                new Object[]{objectMapper.writerWithDefaultPrettyPrinter()
                        .writeValueAsString(
                        new SimpleDataResponse<>(new LinkedList<ObjectNode>() {{
                            add(objectNode);
                        }})), objectMapper.writerWithDefaultPrettyPrinter()
                        .writeValueAsString(BaseResponse.error(500, "返回失败"))});
        WordUtil.getInstance().insertWatermarkText(doc, SecurityUtil.getUserName());
        return doc;
    }

    @Override
    public Map<String, Object> getDataApiDetailById(String id) {
        DataApiEntity dataApiEntity = super.getById(id);
        ApiHeader apiHeader = new ApiHeader();
        MD5Util mt;
        try {
            mt = MD5Util.getInstance();
            apiHeader.setApiKey(mt.encode(id));
            apiHeader.setSecretKey(mt.encode(SecurityUtil.getUserId()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        Map<String, Object> map = new HashMap<>(2);
        map.put("data", dataApiMapper.toVO(dataApiEntity));
        map.put("header", apiHeader);
        return map;
    }

}
